/* blsvn.c -entry point for Burk Labs SVN shell.
 *
 * This is meant to be the simplest/cheapest hack around the
 * Windows svn command-line client that can make me happy.
 * This program has two basic tasks to perform. First, it must
 * keep track of a "current working directory" within repositories
 * (and do so across invocations of this command, requiring some
 * persistent store). Second, it must find references to
 * shortcut repository directories on the command-line and expand them
 * (with respect to the repository "current working directory")
 * into a syntax that the Windows svn command-line client will
 * accept.
 */

#include "blsvn.h"
#include "help.h"
#include "str.h"

#define BLSVN_VERSION   L"0.1"
#define PARAM_SECTION   (L"Params")

/* gather globals into one place */
typedef struct  TGlobal
    {
    int         EchoOnly;
    }           TGlobal;
TGlobal     Global;

enum
    {
    CMD_UNKNOWN,
    CMD_CD,
    CMD_SET,
    CMD_HELP
    };

/* MatchCommand() - translate command string into integer code.
 *
 */
static
int     MatchCommand(LPCWSTR Command)
    {
    int     Result  = CMD_UNKNOWN;

    if(!lstrcmpW(L"cd", Command))
        Result  = CMD_CD;
    else if(!lstrcmpW(L"set", Command))
        Result  = CMD_SET;
    else if(!lstrcmpW(L"help", Command))
        Result  = CMD_HELP;

    return Result;
    }


static
char*    ReadIntoBuffer(HANDLE Handle, DWORD NBytes)
    {
    char*       Result;
    BOOL        ReadStatus;

    Result      = (char*)WcMalloc(NBytes+1);

    ReadStatus  = ReadFile(Handle, Result, NBytes, &NBytes, NULL);
    WCASSERT(ReadStatus != FALSE);

    return Result;
    }

static
WStr*   ConfigPath;

/* GetConfigPath() - locate/create config file.
 *
 * The tricky bits are:
 *     a) have to expand %Appdata\subversion into a path
 *     b) have to initially create our config file as Unicode
 *
 * The standard Windows INI functions will store Unicode strings for us,
 * but only if they notice that the file is already Unicode -- there's no
 * option to force it to be so. 
 */
static
WStr*   GetConfigPath(void)
    {
//    WStr*       Path        = new WStr(L"%AppData%\\subversion\\blconfig");
    WStr*       Path        = new WStr(L"%AppData%\\subversion");
    HANDLE      Found;
    DWORD       Attributes;
    wchar_t*    Comment     = L"# blsvn " BLSVN_VERSION L"\n";

    Path->ExpandEnvVars();
    Attributes  = GetFileAttributesW(Path->WChar());
    if(Attributes == INVALID_FILE_ATTRIBUTES)
        {
        WcErrorW(L"The directory svn should store data in\n(%s)\ndoesn't exist. Is svn really installed on this machine?\n",
                 Path->WChar());
        }
    Path->Append(L"\\blconfig");
    Attributes  = GetFileAttributesW(Path->WChar());
    if(Attributes == INVALID_FILE_ATTRIBUTES)
        {
        WORD        BOM = 0xFEFF;
        DWORD       NBytes;
        DWORD       Error   = GetLastError();

        if(Error != ERROR_FILE_NOT_FOUND)
            WcErrorW(L"Trouble accessing config file: '%s'\n", Path->WChar());
        Found   = CreateFileW(Path->WChar(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
        if(Found == INVALID_HANDLE_VALUE)
            WcErrorW(L"Unable to create config file: '%s'\n", Path->WChar());
        WriteFile(Found, &BOM, sizeof(WORD), &NBytes, NULL);
        WriteFile(Found, Comment, lstrlenW(Comment)*sizeof(Comment[0]), &NBytes, NULL);
        CloseHandle(Found);
        }
    else if((Attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
        WcErrorW(L"%s is a directory, but I need it to be a file!\n", Path->WChar());

    return Path;
    }

static
WStr*    ConfigGet(const wchar_t* Section, const wchar_t* Name, const wchar_t* Path)
    {
    if(Path == NULL)
        {
        if(!ConfigPath)
            ConfigPath  = GetConfigPath();
        Path    = ConfigPath->WChar();
        }
    return WStr::FromProfile(Section, Name, Path);
    }


static
void ConfigPut(const wchar_t* Section, const wchar_t* Name, const wchar_t* Value)
    {
    BOOL        Status;

    if(!ConfigPath)
        ConfigPath  = GetConfigPath();

    Status      = WritePrivateProfileStringW(
                        Section,
                        Name,
                        Value,
                        ConfigPath->WChar()
                        );
    if(Status == FALSE)
        WcErrorW(L"Configuration file: '%s'\nCould not write [%s] %s=%s\n",
                 ConfigPath->WChar(), Section, Name, Value);
    }

/* GetSvnExePath() - obtain path of svn.exe, or die with error message
 */
static
WStr*   GetSvnExePath(void)
    {
    static WStr*    SvnExePath;

    /* if we haven't been called before by this process */
    if(SvnExePath == NULL)
        {
        /* see if we found svn.exe last time this process ran */
        SvnExePath  = ConfigGet(PARAM_SECTION, L"SvnExePath", NULL);

        /* if no or bad stored path for svn.exe */
        if(SvnExePath == NULL || SvnExePath->IsNull() || !PathFileExistsW(SvnExePath->WChar()))
            {
            /* try searching in all the usual directories */
            if(SvnExePath)  /* ensure damn big buffer */
                delete SvnExePath;
            SvnExePath  = new WStr(BLSVN_MAX_PATH);
            SvnExePath->Append(L"svn.exe");
            if(!PathFindOnPathW(SvnExePath->WChar(), NULL))
                {
                delete SvnExePath;
                SvnExePath  = NULL;
                }
            }
        }
    if(SvnExePath == NULL)
        {
        WcFPrintfW(WcStderr, L"Can't locate svn.exe\nUse 'blsvn set SvnExePath somepath\\svn.exe'\n");
        ExitProcess(1);
        }

    return SvnExePath;
    }

/* IsWorkingCopy() - is the given local file path a working copy?
 *
 */
static
int     IsWorkingCopy(const wchar_t* Path)
    {
    WStr*       PathWithSlash;
    WStr*       Temp;
    int         Result      = FALSE;    /* assume failure */
    DWORD       Attributes;

    PathWithSlash   = new WStr(Path);
    PathWithSlash->ForceTrailingSlash(L'\\');

    Temp            = new WStr(*PathWithSlash);
    Temp->Append(L".svn");
    Attributes  = GetFileAttributesW(Temp->WChar());
    delete Temp;
    if(Attributes != INVALID_FILE_ATTRIBUTES
        && (Attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
        {
        Temp            = new WStr(*PathWithSlash);
        Temp->Append(L".svn\\entries");
        Attributes = GetFileAttributesW(Temp->WChar());
        delete Temp;
        if(Attributes != INVALID_FILE_ATTRIBUTES
           && (Attributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
            Result  = TRUE;
        }

    delete PathWithSlash;
    
    return Result;
    }


/* CreateTempFile() - create and return handle to temporary file.
 *
 * Due to lousy pipe support on Windows, we communicate with svn.exe
 * by having it send its stderr and stdout to temporary files. On
 * the plus side, Windows has good tempfile semantics and when all
 * goes well, probably the disk need not be touched at all.
 */
static
HANDLE  CreateTempFile(void)
    {
    WStr*               TempFile;
    HANDLE              Result;
    SECURITY_ATTRIBUTES Secure;

    Secure.nLength              = sizeof(Secure);
    Secure.lpSecurityDescriptor = NULL;
    Secure.bInheritHandle       = TRUE;
    TempFile                    = WStr::FromTempFile();

    Result  = CreateFileW(TempFile->WChar(),
                          GENERIC_READ | GENERIC_WRITE,
                          0,     /* no "sharing", but child can get to file via inheritance*/
                          &Secure,
                          CREATE_ALWAYS,
                          FILE_ATTRIBUTE_TEMPORARY|FILE_FLAG_DELETE_ON_CLOSE,
                          NULL);
    WCASSERT(Result != INVALID_HANDLE_VALUE);

    delete  TempFile;

    return Result;
    }


/* Svn() - execute svn.exe
 *
 * This is the core function to spawn a copy of svn.exe to do our
 * bidding. The caller must supply the command line to pass, but
 * can optionally also pass handles for stdout and stderr so that
 * it can read the output of the command.
 */
static
DWORD   Svn(WStr* CommandLine, char** Output, int EchoOnly)
    {
    STARTUPINFOW            StartupInfo;
    PROCESS_INFORMATION     ProcInfo;
    BOOL                    Status;
    WStr*                   Temp;
    WStr*                   SvnExePath;
    DWORD                   Result  = 0;
    DWORD                   StdOutSize, StdErrSize;
    DWORD                   WaitStatus;

    WCMEMZERO(&StartupInfo);
    StartupInfo.cb  = sizeof(StartupInfo);

    if(Output != NULL) /* if reading child output is required */
        {
        *Output                 = NULL;
        StartupInfo.hStdOutput  = CreateTempFile();
        StartupInfo.hStdError   = CreateTempFile();
        StartupInfo.dwFlags     = STARTF_USESTDHANDLES;
        }
    SvnExePath  = GetSvnExePath();
    Temp        = new WStr(*CommandLine);

    if(EchoOnly)
        {
        WcPrintfW(L"%s\n", CommandLine->WChar());
        ExitProcess(0);
        }

    Status  = CreateProcessW(SvnExePath->WChar(),
                             Temp->WChar(),  /* command line                 */
                             NULL, NULL,    /* no process/thread security   */
                             TRUE,          /* inherit handles              */
                             0,
                             NULL,
                             NULL,
                             &StartupInfo,
                             &ProcInfo
                             );
    WCASSERT(Status != 0);


    WaitStatus = WaitForSingleObject(ProcInfo.hProcess, INFINITE);
    WCASSERT(WaitStatus == WAIT_OBJECT_0);
    
    Status = GetExitCodeProcess(ProcInfo.hProcess, &Result);
    WCASSERT(Status);

    Status = CloseHandle(ProcInfo.hProcess);
    WCASSERT(Status);
    Status = CloseHandle(ProcInfo.hThread);
    WCASSERT(Status);

    if(Output != NULL)
        {
        StdOutSize = GetFileSize(StartupInfo.hStdOutput, NULL);
        StdErrSize = GetFileSize(StartupInfo.hStdError, NULL);
    
        SetFilePointer(StartupInfo.hStdOutput, 0, NULL, FILE_BEGIN);
        SetFilePointer(StartupInfo.hStdError, 0, NULL, FILE_BEGIN);
    
        if(StdErrSize)
            *Output     = ReadIntoBuffer(StartupInfo.hStdError, StdErrSize);
        else
            *Output     = ReadIntoBuffer(StartupInfo.hStdOutput, StdOutSize);
        }

    return Status;
    }

/* SvnInfoDir() - execute 'svn --xml info' on a (presumed) directory.
 */
static
WStr*   SvnInfoDir(const wchar_t* Path, const wchar_t* OrigPath, WStr* Root=NULL)
    {
    char*   Start;
    char*   End;
    DWORD   SvnStatus;
    char*   SvnOutput;
    WStr*   Command;
    WStr*   Url     = NULL;


    Command = new WStr(L"svn --non-interactive --xml info ");
    Command->Append(Path);

    SvnStatus   = Svn(Command, &SvnOutput, FALSE);

    /* if there was an error */
    if(StrCmpNA(SvnOutput, "<?xml", 5))
        {
        if(OrigPath)
            WcErrorW(L"'%s' is not a working copy or a repository!\n", OrigPath);
        else
            WcErrorW(L"'%s':\n%hs\n", Path, SvnOutput);
        }
    /* check XML result from svn to see if we have a directory */
    if(!StrStrA(SvnOutput, "kind=\"dir\"") && !StrStrA(SvnOutput, "kind='dir'"))
        {
        WcFPrintfW(WcStderr, L"'%s': is not a directory in the repository.\n", Path);
        ExitProcess(1);
        }
    else    /* else, need to extract repo path from working copy */
        {
        Start   = StrStrA(SvnOutput, "<url>");
        if(Start != NULL && lstrlenA(Start) > 11)
            {
            Start  += 5;                        /* skip "<url>"     */
            End     = StrStrA(Start, "</url>"); /* locate end tag   */
            if(End != NULL)
                {
                Url = WStr::FromXml8(Start, End);
                }
            }
        }

    if(Url == NULL)
        {
        WcFPrintfW(WcStderr, L"blsvn inexplicably could not locate <url> element in svn XML:\n%hs\n",
                   SvnOutput);
        ExitProcess(1);
        }
    if(Root != NULL)
        {
        int     FoundRoot   = FALSE;
        
        Start   = StrStrA(SvnOutput, "<root>");
        if(Start != NULL && lstrlenA(Start) > 13)
            {
            Start  += 6;                         /* skip "<url>"     */
            End     = StrStrA(Start, "</root>"); /* locate end tag   */
            if(End != NULL)
                {
                *Root       = *WStr::FromXml8(Start, End);
                FoundRoot   = TRUE;
                }
            }
        if(!FoundRoot)
            {
            WcFPrintfW(WcStderr, L"blsvn inexplicably could not locate <root> element in svn XML:\n%hs\n",
                   SvnOutput);
            ExitProcess(1);
            }
        }

    return Url;
    }

/* ExpandDotRelativePath() - expand pattern of '~' followed by one or more dots.
 *
 * This syntax allows one to refer to repository paths relative to the
 * repository directory that is parallel to that of the current directory
 * of the working copy. We locate the repository path that corresponds to
 * the current working directory. If there are multiple dots, we then traverse
 * upwards the corresponding number of directories.
 */
static
WStr* ExpandDotRelativePath(int DotCount, const wchar_t* Path)
    {
    WStr*       Url;
    WStr*       Root;
    int         iDot;
    
    /* cwd must be a working copy! */
    if(!IsWorkingCopy(L"."))
        {
        WcFPrintfW(WcStderr, L"'%s': current directory is not an SVN working copy!\n", Path);
        ExitProcess(1);
        }
    Root    = new WStr(BLSVN_MAX_PATH);
    Url     = SvnInfoDir(L".", NULL, Root);

    WCASSERT(Root->Len() <= Url->Len());
    for(iDot = 1; iDot < DotCount; ++iDot)
        {
        if(Url->Len() > Root->Len())
            Url->TrimDir();
        else
            break;
        }

    return Url;
    }


/* ExpandRepoPath() - translate a "~" path into one SVN likes.
 */
static
WStr*   ExpandRepoPath(const wchar_t* Path)
    {
    const wchar_t*  Rover;
    WStr*           ExpandedAbbrev  = NULL;
    WStr*           Canonical;
    HRESULT         ErrorCode;
    int             DotCount;


    /* don't call us without reason! */
    WCASSERT(Path[0] == ABBREV_CHAR);

    for(Rover = Path; *Rover && !ISSLASH(*Rover); ++Rover)
        ;

    /* if it is a "dot-relative" abbreviation   */
    DotCount    = StrSpnW(Path+1, L".");
    if(DotCount > 0 && DotCount == (Rover - Path) - 1)
        {
        ExpandedAbbrev  = ExpandDotRelativePath(DotCount, Path);
        WCASSERT(ExpandedAbbrev != NULL);
        }
    /* if there is a ~name abbreviation to expand */
    else if(Rover - Path > 1)
        {
        WStr*           Abbrev;
        
        Abbrev      = new WStr(Path, Rover-Path);
    
        ExpandedAbbrev  = ConfigGet(PARAM_SECTION, Abbrev->WChar(), NULL);
    
        if(ExpandedAbbrev == NULL || ExpandedAbbrev->IsNull())
            {
            WcFPrintfW(WcStderr, L"'%s' is not a defined repository directory abbreviation.\n",
                       Abbrev->WChar());
            ExitProcess(1);
            }
        }
    /* else, it was just '~', which refers to our "current repository directory" */
    else
        {
        WCASSERT(Rover - Path == 1);
        ExpandedAbbrev = ConfigGet(PARAM_SECTION, L"~", NULL);
        if(ExpandedAbbrev->IsNull())
            {
            WcFPrintfW(WcStderr, L"'%s': current repository directory not defined.\n"
                       L"Use 'blsvn cd <path>' to set current repository directory.\n",
                       Path);
            ExitProcess(1);
            }
        }
    /* if there was anything after the ~ abbreviation, append to the expansion */
    if(*Rover)
        ExpandedAbbrev->Append(Rover);
    
    /* we hope to escape spaces and handle embedded . and .. elements */
    Canonical   = new WStr(BLSVN_MAX_PATH);
    ErrorCode   = Canonical->UrlCanonicalize(ExpandedAbbrev, URL_ESCAPE_SPACES_ONLY);
    WCASSERT(ErrorCode == S_OK);

    return Canonical;
    }

/* ExpandAndTest() - expand a path and confirm it exists.
 *
 * Some internal commands (such as "cd") want to confirm that a
 * path exists and is acceptable to svn before proceeding. This
 * function performs any abbreviation expansion needed and then
 * tests that the result is valid for svn.exe. If not, it aborts
 * with an error message.
 *
 * Upon success, we return the expanded version of the path.
 */
static
WStr*  ExpandAndTest(const wchar_t* Path)
    {
    const wchar_t*  NeedRepo    = NULL;
    WStr*       Url;
    WStr*       ExpandedPath    = NULL;

    /* if it's a repo-relative path */
    if(Path[0] == ABBREV_CHAR)
        ExpandedPath   = ExpandRepoPath(Path);
    else if(StrHasScheme(Path))
        ExpandedPath   = new WStr(Path);
    /* else, we hope it's a file-system path */
    else if(GetFileAttributesW(Path) == INVALID_FILE_ATTRIBUTES)
        {
        DWORD   Error = GetLastError();
        
        if(Error == ERROR_FILE_NOT_FOUND || Error == ERROR_PATH_NOT_FOUND)
            {
            WcFPrintfW(WcStderr, L"'%s': does not exist.\n", Path);
            ExitProcess(1);
            }
        else
            WcErrorW(L"'%s': unable to access.\n", Path);
        }
    /* else, it's a filesystem path, but what does it mean? */
    else
        {
        /* do not modify path if it looks like a working copy */
        if(IsWorkingCopy(Path))
            ExpandedPath   = new WStr(Path);
        /* else, make it a URL in hopes it is a repository reference */
        else
            {
            ExpandedPath    = WStr::UrlFromPath(Path);
            NeedRepo        = Path;
            }
        }

    WCASSERT(ExpandedPath != NULL);
    
    /* now we need to call svn.exe in secret to see if
     * this path is acceptable...
     */
    Url = SvnInfoDir(ExpandedPath->WChar(), NeedRepo);

    return Url;
    }

/* cd - change working repository directory.
 *
 * If the argument starts with a scheme that isn't file:, then leave it alone.
 * 
 * If the argument starts with a "~", expand it.
 * If expanded argument is a working copy, translate
 * it to a repo path.
 * else if expanded argument is a repository
 */
void    CommandCd(int ArgCount, LPWSTR* Args)
    {
    WStr*       NewCurDir   = NULL;

    if(ArgCount > 3)
        {
        WcFPrintfW(WcStderr, L"cd: too many arguments. Try 'blsvn help cd'.\n");
        ExitProcess(1);
        }

    WCASSERT(ArgCount == 3);
    NewCurDir   = ExpandAndTest(Args[2]);

    if(NewCurDir && !NewCurDir->IsNull())
        {
        ConfigPut(PARAM_SECTION, L"~", NewCurDir->WChar());
        }
    }


void    CommandPwd(void)
    {
    WStr*   Cwd;

    Cwd = ConfigGet(PARAM_SECTION, L"Cwd", NULL);
    if(Cwd == NULL || Cwd->IsNull())
        {
        WcPrintfW(L"current directory not defined; "
                  L"use 'blsvn cd <path>' to define it.\n");
        }
    else
        WcPrintfW(L"%s\n", Cwd->WChar());
    }

/* CommandSet() - set or display persistent variables.
 *
 * ??? don't allow illegal names, especially names containing only dots!
 */
void    CommandSet(int ArgCount, LPWSTR* Args)
    {
    if(ArgCount > 4)
        {
        WcFPrintfW(WcStderr, L"Too many arguments to 'set' command. Try 'blsvn help set'\n");
        ExitProcess(1);
        }
    /* if using 'set' to define a tilde abbreviation... */
    if(Args[2][0] == ABBREV_CHAR)
        {
        WStr*           Url;
        const wchar_t*  Path    = Args[3];
        
        Url     = ExpandAndTest(Path);
        ConfigPut(PARAM_SECTION, Args[2], Url->WChar());
        delete Url;
        }
    else if(lstrcmpW(Args[2], L"SvnExePath"))
        {
        WcFPrintfW(WcStderr, L"'%s' is not a defined internal variable. Did you mean '~%s'?\n"
                   L"Try 'blsvn help set' for more information.\n",
                   Args[2], Args[2]);
        ExitProcess(1);
        }
    else
        ConfigPut(PARAM_SECTION, Args[2], Args[3]);
    }

/* CommandPrintSet() - print a particular 'set' variable.
 */
void    CommandPrintSet(wchar_t* VarName)
    {
    WStr*   Path;

    Path    = ConfigGet(PARAM_SECTION, VarName, NULL);

    if(Path->IsNull())
        WcFPrintfW(WcStderr, L"'%s' is not defined.\n", VarName);
    else
        WcPrintfW(L"%s=%s\n", VarName, Path->WChar());
    delete Path;
    }

/* CommandPrintSetAll() - print out all settable variables.
 */
void    CommandPrintSetAll(void)
    {
    WStr*       Names;
    wchar_t*    Rover;

    Names   = ConfigGet(PARAM_SECTION, NULL, NULL);
    Rover   = Names->WChar();
    while(*Rover)
        {
        CommandPrintSet(Rover);
        Rover   += lstrlenW(Rover)+1;
        WCASSERT(Rover < Names->Sentinel());
        }
    }

/* CommandHelp() - user wants some help text.
 *
 * There are multiple cases to handle. If no subcommand name is supplied to
 * get help on, then we mention how to get blsvn-specific help before turning
 * it over to svn.exe to provide default help.
 *
 * Otherwise, for each subcommand name we know about, we emit the help text.
 * 
 * Finally, if there are any subcommand names we don't know about, we pass
 * those on to svn.exe.
 */
void    CommandHelp(int ArgCount, LPWSTR* Args)
    {
    if(ArgCount == 2)
        {
        WStr   Cmd = L"svn help";
        WcPrintfW(L"(use blsvn help [set|cd] for help on blsvn subcommands.)\n");
        Svn(&Cmd, NULL, FALSE);
        }
    else if(ArgCount > 2)
        {
        int     Unknowns;
        int     iArg;
        int     CommandId;

        Unknowns    = 0;
        for(iArg = 2; iArg < ArgCount; ++iArg)
            {
            CommandId   = MatchCommand(Args[iArg]);
            switch(CommandId)
                {
                case    CMD_CD          :   HelpEmit(HelpCd);       break;
                case    CMD_SET         :   HelpEmit(HelpSet);      break;
                default:
                    {
                    WStr   Cmd = L"svn help ";
                    Cmd.Append(Args[iArg]);
                    Svn(&Cmd, NULL, FALSE);
                    }
                }
            }
        }
    }

/* CommandSvn() - execute an external command.
 *
 * The command is not a blsvn command, so we need to pass it
 * along (almost) as-is to svn.exe. The "almost" arises in two
 * cases. First, a leading argument of "-" must be deleted, since
 * that is always our private flag that means "only echo the call to svn".
 * Second, any arguments that start with "~" are assumed to refer
 * to a previously defined repository path that we must expand.
 */
void    CommandSvn(void)
    {
    LPWSTR*         Args;
    LPWSTR          CommandLine;
    int             ArgCount;
    int             iArg;
    const wchar_t*  Arg;
    WStr*           ExpandedCommand;
    

    CommandLine = GetCommandLineW();
    WCASSERT(CommandLine != NULL);
    
    Args    = CommandLineToArgvW(CommandLine, &ArgCount);
    WCASSERT(Args != NULL);
    
    ExpandedCommand = new WStr(L"svn");
    for(iArg = 1; iArg < ArgCount; ++iArg)
        {
        WStr*   QuotedArg;

        Arg         = Args[iArg];

        if(iArg == 1 && !lstrcmpW(Arg, L"-"))
            continue;
        ExpandedCommand->Append(L" ");
        if(Arg[0] == ABBREV_CHAR)
            {
            WStr*       Path = ExpandRepoPath(Arg);
            QuotedArg  = WStr::Quoted(Path->WChar());
            delete Path;
            }
        else
            QuotedArg  = WStr::Quoted(Arg);

        ExpandedCommand->Append(QuotedArg);
        delete QuotedArg;
        }
    Svn(ExpandedCommand, NULL, Global.EchoOnly);
    }


static
int     IsHelpOption(LPWSTR Str)
    {
    int     Result  = FALSE;
    
    if(!lstrcmpW(Str, L"-?") || !lstrcmpW(Str, L"-h") || !lstrcmpW(Str, L"--help"))
        Result      = TRUE;

    return Result;
    }


static
void    ShiftArgsLeft(LPWSTR* Args, int CurPos, int ArgCount)
    {
    int     iArg;

    WCASSERT(CurPos < ArgCount);
    for(iArg = CurPos; iArg < (ArgCount-1); ++iArg)
        Args[iArg]  = Args[iArg+1];
    }

/* ParseCommandLine() - break it up, reorder, etc.
 *
 * First important assumption: our internal commands don't have options that
 * take arguments, and they don't mind having options shoved to the end of the
 * argument stack. Second important assumption: we will do completely separate
 * parsing when we are trying to simply pass a command along (possibly with
 * abbreviation expansion) to svn.exe as-is.
 *
 * Note that any subcommand may turn out to be a "help" subcommand if an
 * option of "-?", "-h", or "--help" is encountered at (almost) any point.
 *
 * We obey the svn.exe convention that an argument of "--" marks the end
 * of any options (allowing the user to refer to names that start with
 * "-" without having them treated as illegal options).
 */
static
LPWSTR* ParseCommandLine(int* CommandId, int* ArgCount, int* OptionCount)
    {
    LPWSTR*     Args;
    LPWSTR      CommandLine;
    LPWSTR      Arg;
    LPWSTR      Command         = NULL;
    int         OptionsValid    = TRUE;
    int         IsHelpCommand   = FALSE;
    int         iArg;

    CommandLine = GetCommandLineW();
    WCASSERT(CommandLine != NULL);
    
    Args    = CommandLineToArgvW(CommandLine, ArgCount);
    WCASSERT(Args != NULL);
    
    *OptionCount    = 0;
    *CommandId      = CMD_UNKNOWN;
    /* first, look for our special "echo" argument */
    if(*ArgCount > 1 && !lstrcmpW(Args[1], L"-"))
        {
        Global.EchoOnly = TRUE;
        /* eat this argument */
        ShiftArgsLeft(Args, 1, *ArgCount);
        --*ArgCount;
        }

    for(iArg = 1; iArg < *ArgCount; ++iArg)
        {
        Arg     = Args[iArg];
        
        /* a lone double-dash marks the end of all options */
        if(!lstrcmpW(Arg, L"--"))
            {
            OptionsValid    = FALSE;
            /* eat this argument */
            ShiftArgsLeft(Args, iArg, *ArgCount+*OptionCount);
            --iArg;
            --*ArgCount;
            }
        /* else, if it looks like an option     */
        else if(OptionsValid && Arg[0] == L'-')
            {
            ShiftArgsLeft(Args, iArg, *ArgCount+*OptionCount);
            if(IsHelpOption(Arg))
                IsHelpCommand   = TRUE;
            else
                {
                /* move option to the end of the list
                 * remember: this will only affect internal commands
                 */
                Args[*ArgCount + *OptionCount - 1]  = Arg;
                ++*OptionCount;
                }
            --iArg;
            --*ArgCount;
            }

        else if(Command == NULL)
            Command     = Arg;
        }

    if(Command != NULL)
        *CommandId       = MatchCommand(Command);

    /* if an option forces this to be a "help" subcommand */
    if(IsHelpCommand)
        {
        *CommandId   = CMD_HELP;

        /* insert a fake "help" subcommand to make life easier later */
        for(iArg = *ArgCount+*OptionCount-1; iArg > 0; --iArg)
            Args[iArg+1]    = Args[iArg];
        Args[1]     = L"help";
        ++*ArgCount;
        }

    return Args;
    }

/* Microsoft C stuff to avoid default libraries and specify
 * a raw entry point.
 */
#pragma comment(linker, "/entry:WinEntry")
#pragma comment(linker, "/subsystem:console")
#pragma comment(linker, "/NODEFAULTLIB")
/* don't really need that default 1MB stack */
#pragma comment(linker, "/stack:0xFFFF,0x1FFF")


void    WinEntry(void)
    {
    LPWSTR*     Args;
    int         ArgCount, OptionCount, CommandId;

    WcInit();

    Args    = ParseCommandLine(&CommandId, &ArgCount, &OptionCount);

    if(ArgCount == 1 && OptionCount == 0)
        HelpEmit(HelpMain);
    else
        {
        switch(CommandId)
            {
            case    CMD_CD  :
                if(ArgCount > 2 && Args[1][0] != L'-')
                    CommandCd(ArgCount, Args);
                else
                    CommandPwd();
                break;
            case    CMD_SET  :
                if(ArgCount == 2)
                    CommandPrintSetAll();
                else if(ArgCount == 3)
                    CommandPrintSet(Args[2]);
                else 
                    CommandSet(ArgCount, Args);
                break;
            case    CMD_HELP  :
                CommandHelp(ArgCount, Args);
                break;
            default:
                WCASSERT(CommandId == CMD_UNKNOWN);
                CommandSvn();
            }
        }
    }

